package domein;

import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Observable;
import java.util.Observer;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Principal class handling all Client operations.
 *
 * @author Colin Bundervoet
 * @author Jeroen Pattyn
 */
public class Client implements Observer
{
    private String server;
    private int port;
    private String username;
    private ObjectInputStream oiStream;
    private PrintWriter writer;
    private Socket socket;
    private MessageObservable messageObservable;
    private OnlineUsersObservable onlineUsersObservable;
    private StateObservable stateObservable;
    private ServerListener serverListener;
    private boolean connected;
    private boolean disconnecting;
    private boolean serverDisconnect;
    private Thread thread;
    private ReceivingFileObservable receivingFile;
    private FileReceiver fileReceiver;
    private SendingFileObservable sendingFile;
    private FileSender fileSender;

    /**
     * Default constructor for Client class. Sets all observers.
     */
    public Client() {
        messageObservable = new MessageObservable();
        onlineUsersObservable = new OnlineUsersObservable();
        stateObservable = new StateObservable();
        receivingFile = new ReceivingFileObservable();
        sendingFile = new SendingFileObservable();
        addSendingFileObserver(this);
        addReceivingFileObserver(this);
    }

    /**
     * Adds observer to update gui of new messages.
     *
     * @param o Observer to observe Client
     */
    public void addMessageObserver(Observer o) {
        messageObservable.addObserver(o);
    }

    /**
     * Adds observer to update gui of online users
     *
     * @param o Observer to observe Client
     */
    public void addOnlineUsersObserver(Observer o) {
        onlineUsersObservable.addObserver(o);
    }

    /**
     * Adds observer to update gui of client connection state
     *
     * @param o Observer to observe Client
     */
    public void addStateObserver(Observer o) {
        stateObservable.addObserver(o);
    }

    /**
     * Adds observer to update gui of file receiving events.
     *
     * @param o Observer to observe Client
     */
    public void addReceivingFileObserver(Observer o) {
        receivingFile.addObserver(o);
    }

    /**
     * Adds observer to update gui of file sending events.
     *
     * @param o
     */
    public void addSendingFileObserver(Observer o) {
        sendingFile.addObserver(o);
    }

    /**
     * Sets the String value for the server IP.
     *
     * @param server server IP as a String
     */
    private void setServer(String server) {
        if (!server.isEmpty()) {
            this.server = server;
        }
        else {
            throw new IllegalArgumentException("You have to select a server.");
        }
    }

    /**
     * Sets the port value for the connection server.
     *
     * @param port integer value for port.
     * @throws PortOutOfRangeException when not a valid port
     */
    private void setPort(int port) throws PortOutOfRangeException {
        if (port > 0 && port < 65536) {
            this.port = port;
        }
        else {
            throw new PortOutOfRangeException(port, 1, 65535);
        }
    }

    /**
     * Sets the desired initial connection username for this client.
     *
     * @param username String name for this user.
     */
    private void setUsername(String username) {
        if (!username.isEmpty()) {
            this.username = username;
        }
        else {
            throw new IllegalArgumentException("You have to select a username.");
        }
    }

    /**
     * Sets the current connection state boolean. Notifies gui.
     *
     * @param connected boolean for connected
     */
    private void setConnected(boolean connected) {
        this.connected = connected;
        stateObservable.updateState(connected);
    }

    /**
     * Connects the client to a given server. Packs the inside its own thread to
     * run.
     *
     * @param server server ip to connect to.
     * @param port port to connect over
     * @param username desired username.
     */
    public void connect(String server, String port, String username) {
        try {
            final String s = server;
            final int p = Integer.parseInt(port);
            final String u = username;
            Thread t = new Thread()
            {
                @Override
                public void run() {
                    connect(s, p, u);
                }
            };
            t.start();
        }
        catch (NumberFormatException ex) {
            putMessage("Portnumber has to be a number in this range [1-65535]");
        }
    }

    /**
     * private implementation for connection.
     *
     * @param server String representation of ip
     * @param port integer port number
     * @param username desired initial username.
     */
    private void connect(String server, int port, String username) {
        if (!connected) {
            try {
                setServer(server);
                setPort(port);
                setUsername(username);
                disconnecting = false;
                serverDisconnect = false;
                socket = new Socket(server, port);
                oiStream = new ObjectInputStream(socket.getInputStream());
                writer = new PrintWriter(socket.getOutputStream());
                setConnected(true);
                sendMessage("/nick " + username);
                putMessage("Connection established: " + socket.getInetAddress().toString());
                serverListener = new ServerListener();
                thread = new Thread(serverListener);
                thread.start();
            }
            catch (IllegalArgumentException ex) {
                putMessage(ex.getMessage());
            }
            catch (IOException ex) {
                putMessage("Cannot connect to " + server + ". Please try again");
            }
        }
        else {
            putMessage("You are already connected!");
        }
    }

    /**
     * Disconnects this client from the server. Invoke when voluntary
     * disconnection happens.
     */
    public void disconnect() {
        if (connected) {
            try {
                disconnecting = true;
                if (!serverDisconnect) {
                    putMessage("You are disconnected");
                    sendMessage("/dc");
                }
                writer.close();
                oiStream.close();
                serverListener = null;
                socket.close();
                socket = null;
                thread = null;
                setConnected(false);
            }
            catch (IOException ex) {
            }
        }
    }

    /**
     * Sends a message to the server only if it is a filetransfer message.
     *
     * @param message filetransfer request message.
     */
    void send(String message) {
        try {
            if (message.startsWith("/filetransfer")) {
                writer.println(message);
                writer.flush();
            }
        }
        catch (Exception ex) {
            putMessage("An error occured, message was not sent");
        }
    }

    /**
     * Sends a raw message to the server.
     *
     * @param message String message to be sent.
     */
    public void sendMessage(String message) {
        try {
            String[] lines = message.split("\n");
            for (int i = 0; i < lines.length; i++) {
                if (lines[i].equals("")) {
                    writer.println(" ");
                }
                else {
                    if (lines[i].startsWith("/filetransfer")) {
                        fileSender = new FileSender(this, lines[i]);
                        fileSender.start(0, sendingFile);
                    }
                    else {
                        writer.println(lines[i]);
                    }
                }
                writer.flush();
            }
        }
        catch (Exception ex) {
            putMessage("An error occured, message was not sent");
        }
    }

    /**
     * places messages from anywhere and sends a notification to the gui to
     * update.
     *
     * @param message
     */
    public void putMessage(String message) {
        SimpleDateFormat date = new SimpleDateFormat();
        String time = date.format(new Date());
        messageObservable.addMessage(message);
    }

    /**
     * Makes FileSender wait for a FileReceiver to connect and sends the
     * selected file.
     *
     * @param selectedFile
     */
    public void startSendingFile(File selectedFile) {
        fileSender.send(selectedFile);
    }

    /**
     * Starts the connection of FileReceiver and starts receiving the file.
     *
     * @param selectedFile
     */
    public void connectFileReceiver(File selectedFile) {
        fileReceiver.connect(selectedFile);
    }

    @Override
    public void update(Observable o, Object arg) {
        if (o.getClass() == ReceivingFileObservable.class) {
            if (((ReceivingFileObservable) o).getProgres() == -1) {
                putMessage("File received.");
            }
        }
        else if (o.getClass() == SendingFileObservable.class) {
            if (((SendingFileObservable) o).getProgres() == -1) {
                putMessage("File sent.");
            }
        }
    }

    /**
     * Private inner class to listen to messages from the server. Should be
     * invoked inside a thread.
     */
    private class ServerListener implements Runnable
    {
        Message message;

        /**
         * Run method for this Runnable instance of ServerListener. Will
         * correctly interpret the Message received.
         */
        @Override
        public void run() {
            try {
                while ((message = (Message) oiStream.readObject()) != null) {
                    if (message.type.equals(MessageType.ONLINEUSERS)) {
                        String[] users = message.value.split(",");
                        onlineUsersObservable.updateOnlineUsers(users);
                    }
                    else {
                        if (message.type.equals(MessageType.FILEMETADATA)) {
                            MessageFileMetaData mfmd = (MessageFileMetaData) message;
                            fileReceiver = new FileReceiver(mfmd.getIp(), mfmd.getPort(), mfmd.getValue(), mfmd.getFilesize(), receivingFile);
                            String message = mfmd.getSendername() + " is trying to send you a file named " + mfmd.getValue();
                            putMessage(message);
                            receivingFile.update(message, 0);
                        }
                        else {
                            putMessage(message.value);
                        }
                    }
                }
            }
            catch (ClassNotFoundException ex) {
                Logger.getLogger(Client.class.getName()).log(Level.SEVERE, null, ex);

            }
            catch (IOException ex) {
                if (!disconnecting) {
                    putMessage("An error occurred while connecting to the server, connection lost");
                    serverDisconnect = true;
                    disconnect();
                }
            }
        }
    }
}
